# -*- coding: utf-8 -*-
"""
中性策略框架 | 邢不行 | 2024分享会
author: 邢不行
微信: xbx6660
"""
import os
import pandas as pd
from program.Config import factor_class_list

# 取出策略文件名中_后的部分
stg_name = os.path.basename(__file__).split('.')[0].split('_')[1]

# 持仓周期。目前回测支持日线级别、小时级别。例：1H，6H，3D，7D......
# 当持仓周期为D时，选币指标也是按照每天一根K线进行计算。
# 当持仓周期为H时，选币指标也是按照每小时一根K线进行计算。
hold_period = '1D'.replace('h', 'H').replace('d', 'D')
# 配置offset
offset = 0
# 是否使用现货
if_use_spot = True  # True：使用现货。False：不使用现货，只使用合约。

# 多头选币数量。1 表示做多一个币; 0.1 表示做多10%的币
long_select_coin_num = 0.1
# 空头选币数量。1 表示做空一个币; 0.1 表示做空10%的币
# short_select_coin_num = 0.1
short_select_coin_num = 'long_nums'  # long_nums意为着空头数量和多头数量保持一致。最多为所有合约的数量。注意：多头为0的时候，不能配置'long_nums'

# 多头的选币因子列名。
long_factor = '因子'  # 因子：表示使用复合因子，默认是 factor_list 里面的因子组合。需要修改 calc_factor 函数配合使用
# 空头的选币因子列名。多头和空头可以使用不同的选币因子
short_factor = '因子'

############################################
# 由贝叶斯炼丹炉自动生成的代码
############################################

# @$开始定义选币炼丹结果数据$@#
# 炼丹时间: 2024-08-04 16:13:51.857464
# 基础策略: Strategy_筛选因子炼丹
# 迭代次数: 814
# 炼丹得分: -231.72 排名: 1
# 回测数据: {
#     "中性_回测结果": {
#         "累积净值": 3.4,
#         "年化收益": "63.46%",
#         "最大回撤": "-11.50%",
#         "年化收益/回撤比": 5.52,
#         "胜率": "30.49%",
#         "盈亏收益比": 2.56,
#         "单周期最大盈利": "7.79%",
#         "单周期大亏损": "-3.40%",
#         "最大连续盈利周期数": 17.0,
#         "最大连续亏损周期数": 8761.0,
#         "每周期平均换手率": "13.59%",
#         "收益率标准差": "0.21%",
#         "2021": "0.0%",
#         "2022": "161.88%",
#         "2023": "30.0%"
#     },
#     "中性_评分明细": {
#         "年化收益": 63.46,
#         "年化收益_得分": -12.18,
#         "年化收益/回撤比": 5.52,
#         "年化收益/回撤比_得分": -5.6,
#         "胜率": 30.49,
#         "胜率_得分": -19.51
#     },
#     "多头_回测结果": {
#         "累积净值": 2.71,
#         "年化收益": "49.07%",
#         "最大回撤": "-67.66%",
#         "年化收益/回撤比": 0.73,
#         "胜率": "32.21%",
#         "盈亏收益比": 2.2,
#         "单周期最大盈利": "15.35%",
#         "单周期大亏损": "-14.90%",
#         "最大连续盈利周期数": 12.0,
#         "最大连续亏损周期数": 8760.0,
#         "每周期平均换手率": "14.35%",
#         "收益率标准差": "0.60%",
#         "2021": "0.0%",
#         "2022": "51.24%",
#         "2023": "78.92%"
#     },
#     "多头_评分明细": {
#         "年化收益": 49.07,
#         "年化收益_得分": -150.31,
#         "年化收益/回撤比": 0.73,
#         "年化收益/回撤比_得分": -8.54,
#         "胜率": 32.21,
#         "胜率_得分": -35.58
#     }
# }

factor_list = [
    ('ILLQStd', True, 20, 7),
]

filter_list = [
    ('Biasv3', 3),
]

bys_result_user_params = {
    "$过滤因子$_排序方式": False,
    "$过滤因子$_filter_rank": 0.7
}

# @$结束定义选币炼丹结果数据$@#

############################################
# 用于贝叶斯炼丹的参数
############################################

# 炼丹使用过滤因子数量的最小和最大值，如果为 None 则使用 bys_config.py 中的配置
bys_filter_factor_min_count = 1
bys_filter_factor_max_count = 1

# 排序因子参数的默认值，如果为 None 则使用 bys_config.py 中的配置
bys_sort_factor_default = None

# 炼丹使用排序因子数量的最小和最大值，如果为 None 则使用 bys_config.py 中的配置
bys_sort_factor_min_count = 1
bys_sort_factor_max_count = 2

# 回测结果评分参数，如果为 None 则使用 bys_config.py 中的配置
bys_eval_score_dict = None

# 用户自定义参数
# 由于用于过滤因子的排序方式和过滤比例参数是跟具体因子相关的，需要为每一个因子指定一个参数名称，
# 以避免贝叶斯炉子预测参数混乱，所以下面的定义在参数名称中使用了 $过滤因子$ 宏。
bys_user_param_dict = {
    "$过滤因子$_排序方式": {
        "枚举列表": [False, True]
    },
    "$过滤因子$_filter_rank": {
        "下限": 0.70,
        "上限": 0.95,
        "步长": 0.05,
    },
}

def after_merge_index(df, symbol, factor_dict, data_dict):
    """
    合并指数数据之后的处理流程，非必要。
    本函数住要的作用如下：
            1、指定K线上不常用的数据在resample时的规则，例如：'taker_buy_quote_asset_volume': 'sum'
            2、合并外部数据，并指定外部数据在resample时的规则。例如：（伪代码）
                    chain_df = pd.read_csv(chain_path + symbol)  # 读取指定币种链上数据
                    df = pd.merge(df,chain_df,'left','candle_begin_time') # 将链上数据合并到日线上
                    factor_dict['gas_total'] = 'sum' # 链上的gas_total字段，在小时数据转日线数据时用sum处理
                    data_dict['gas_total'] = 'sum' # 链上的gas_total字段，在最后一次resample中的处理规则
    :param df:
    :param symbol:
    :param factor_dict: 小时级别resample到日线级别时使用(计算日线级别因子时需要，计算小时级别因子时不需要)
    :param data_dict: resample生成最终数据时使用
    :return:
    """

    # 专门处理转日线是的resample规则
    factor_dict['taker_buy_quote_asset_volume'] = 'sum'  # 计算日线级别因子前先resample到日线数据
    factor_dict['trade_num'] = 'sum'

    return df, factor_dict, data_dict


def after_resample(df, symbol):
    """
    数据重采样之后的处理流程，非必要
    :param df:          传入的数据
    :param symbol:      币种名称
    :return:
    """

    return df


# =====================以上是数据整理部分封装转的策略代码==========================
# ============================================================================
# ============================================================================
# ============================================================================
# ============================================================================
# =======================以下是选币函数封装的策略代码=============================

# 炼丹时只加载必须用到的因子，此处需要列出在 calc_factor 中使用到的因子，
# 以避免这些因子没有被保留。
user_keep_cols = []

def user_cal_score(
    score,
    tp,
    rtn: pd.DataFrame,
    year_return: pd.DataFrame,
    month_return: pd.DataFrame,
    ret_dict: dict,
    core_dict: dict,
    is_sample: bool,
):
    """
    用户自定义回测评分函数，可以根据自己的需求进行修改
    :param score: 炉子根据配置计算的评分结果
    :param tp: 回测结果类型：中性、多头、空头
    :param rtn: 回测结果
    :param year_return: 年回报数据
    :param month_return: 月回报数据
    :param ret_dict: 保存到 学习结果.csv 文件中的数据
    :param core_dict: 保存到 高分数据.txt 文件中的数据
    :param is_sample: 如果为 True 表示计算炼丹时的样本日期内评分，如果为 False 表示是样本外日期回测
    :return: 计算完成的评分结果
    """

    """
    # 以下代码演示了在总分中增加基于月回报波动率的得分
    # 1.计算月波动率
    month_std_mean = month_return['涨跌幅'].map(lambda x: float(x.replace('%', ''))).std() / \
        month_return['涨跌幅'].map(lambda x: float(x.replace('%', ''))).mean()

    # 2.计算得分
    h_val = 1
    l_val = 3
    h_score = 30
    month_score = (month_std_mean - l_val) / (h_val - l_val) * h_score

    # 3.返回结果
    ret_dict['月回报波动'] = month_std_mean
    core_dict[f'月回报波动_得分'] = round(month_score, 2)
    score += month_score
    """
    return score, ret_dict, core_dict

def calc_factor(df, **kwargs):
    """
    计算因子

    多空使用相同的两个因子

        1。多空相同的单因子选币，不需要编写代码

        2。多空相同的复合因子，本案例代码不需要修改，直接使用即可。《本案例代码就是多空相同的复合因子选币》

    多空使用不同的两个因子

        1。多空不同的单因子选币，不需要编写代码

        2。多空分离选币，需要根据具体因子具体改写
            !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            !!!!!这里改写需要一定的代码能力!!!!!
            !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            多空存在某一方是复合因子，另一方是单因子，则只需要计算一个复合因子即可。代码可以参考下方案例代码

            如果多空都是复合因子，多空需要分别是计算复合因子。

    :param df:          原数据
    :return:
    """
    # 接受外部回测的因子列表，这里主要是去适配`4_遍历选币参数.py`
    external_list = kwargs.get('external_list', [])
    if external_list:  # 如果存在外部回测因子列表，则使用外部因子列表
        _factor_list = external_list
    else:  # 如过不存在外部回测因子列表，默认使用当前策略的因子列表
        _factor_list = factor_list

    # 多空相同的复合因子计算
    if long_factor == short_factor == '因子':
        df[long_factor] = 0
        for factor_name, if_reverse, parameter_list, weight in _factor_list:
            col_name = f'{factor_name}_{str(parameter_list)}'
            # 计算单个因子的排名
            df[col_name + '_rank'] = df.groupby('candle_begin_time')[col_name].rank(ascending=if_reverse, method='min')
            # 将因子按照权重累加
            df[long_factor] += (df[col_name + '_rank'] * weight)

    return df


def before_filter(df, **kwargs):
    """
    前置过滤函数
    自定义过滤规则，可以对多空分别自定义过滤规则

    :param df:                  原始数据
    :return:                    过滤后的数据
    """
    # 接受外部回测的因子列表，这里主要是去适配`5_查看历年参数平原.py`
    ex_filter_list = kwargs.get('ex_filter_list', [])
    if ex_filter_list:  # 如果存在外部回测因子列表，则使用外部因子列表
        _filter_list = ex_filter_list
    else:  # 如过不存在外部回测因子列表，默认使用当前策略的因子列表
        _filter_list = filter_list

    df_long = df.copy()
    df_short = df.copy()

    # 使用过滤列表进行过滤
    for factor, param in _filter_list:
        filter_factor = f"{factor}_{str(param)}"
        # 配置了过滤因子信息，则进行过滤操作
        # 从用户参数字典中获取过滤因子的排序方式和过滤排名，参数字典在炼丹期间由炉子设置，炼丹完成后输出到策略 py 文件中
        ascending = bys_result_user_params.get(f"{filter_factor}_排序方式", True)
        filter_rank = bys_result_user_params.get(f"{filter_factor}_filter_rank", 0.8)
        df_long['filter_rank'] = df_long.groupby('candle_begin_time')[filter_factor].rank(ascending=ascending, pct=True)
        df_long = df_long[(df_long['filter_rank'] < filter_rank)]

        df_short['filter_rank'] = df_short.groupby('candle_begin_time')[filter_factor].rank(ascending=ascending, pct=True)
        df_short = df_short[(df_short['filter_rank'] < filter_rank)]

    return df_long, df_short
